from datetime import datetime
import hashlib
from werkzeug.security import generate_password_hash,check_password_hash
from authlib.jose import jwt,JoseError
from flask import current_app,request
from flask_login import UserMixin,AnonymousUserMixin
from app import db,login_manager

class User(UserMixin,db.Model):
    __tablename__='users'
    id=db.Column(db.Integer,primary_key=True)
    name=db.Column(db.String(7),unique=True)
    b_admin=db.Column(db.Boolean,default=False)
    password_hash=db.Column(db.String(128))
    note_name=db.Column(db.String(10))
    create_time=db.Column(db.DateTime(),default=datetime.now)
    last_seen=db.Column(db.DateTime(),default=datetime.utcnow)
    avatar_hash=db.Column(db.String(32))

    def __init__(self,**kwargs):
        super(User,self).__init__(**kwargs)
        if self.avatar_hash is None:
            self.avatar_hash=hashlib.md5(self.name.encode('utf-8')).hexdigest()
    
    @property
    def password(self):
        raise AttributeError('password is not a readable attribute')
    
    @password.setter
    def password(self,password):
        self.password_hash=generate_password_hash(password)

    def verify_password(self,password):
        return check_password_hash(self.password_hash,password)
    
    def generate_confirmation_token(self,expiration=3600):
        s=current_app.config['SECRET_KEY']
        header={'alg':'HS256'}
        return jwt.encode(header=header,payload={'confirm':self.id},key=s)
    
    # def generate_reset_token(self,expiration=3600):
    #     s=Serializer(current_app.config['SECRET_KEY'],expiration)
    #     return s.dumps({'rest':self.id})
    
    # def reset_password(self,token,new_password):
    #     s=Serializer(current_app.config['SECRET_KEY'])
    #     try:
    #         data=s.loads(token)
    #     except:
    #         return False
    #     if data.get('reset')!=self.id:
    #         return False
    #     self.password=new_password
    #     db.session.add(self)
    #     try:
    #         db.session.commit()
    #     except:
    #         db.session.rollback()
    #     return True
    
    def ping(self):
        self.last_seen=datetime.utcnow()
        db.session.add(self)
        try:
            db.session.commit()
        except:
            print('无法更新用户信息到后台')
            db.session.rollback()
    
    def gravatar(self,size=100,default='identicon',rating='g'):
        if request.is_secure:
            url='https://secure.gravatar.com/avatar'
        else:
            url='http://cravatar.cn/avatar/'
        hash=self.avatar_hash or hashlib.md5(self.name.encode('utf8')).hexdigest()
        return f'{url}/{hash}?s={size}&d={default}&r={rating}'
    
    def can(self,permissions):
        return True
    
    def is_administrator(self):
        return self.b_admin
    
class AnonymousUser(AnonymousUserMixin):
    def can(self,permissions):
        return True
    
    def is_administrator(self):
        return False
    
login_manager.anonymous_user=AnonymousUser

@login_manager.user_loader
def load_user(user_id):
    return User.query.get(int(user_id))

class BTHost(db.Model):
    __tablename__='bt_hosts'
    id=db.Column(db.Integer,primary_key=True)
    ip=db.Column(db.String(15))
    port=db.Column(db.Integer,default=5000)
    last_active=db.Column(db.DateTime(),default=datetime.now)
    b_connect=db.Column(db.Boolean,default=False)
    devices=db.relationship('BTDevice',backref='bt_host',lazy='dynamic')

    def __repr__(self):
        return f'<BTHost {self.ip}:{self.port}>'
    
    def active(self):
        self.last_active=datetime.now()
        db.session.add(self)
        try:
            db.session.commit()
        except:
            db.session.rollback()
    
class BTDevice(db.Model):
    __tablename__='bt_devices'
    id=db.Column(db.Integer,primary_key=True)
    name=db.Column(db.String(20))
    device_id=db.Column(db.Integer)
    bt_host_id=db.Column(db.Integer,db.ForeignKey('bt_hosts.id'))
    max_frequency=db.Column(db.String(10))
    memory_size=db.Column(db.String(10))
    description=db.Column(db.Text())
    # device_description_id=db.Column(db.Integer,db.ForeignKey('device_descriptions.id'))

    def __repr__(self):
        return f'<BTDevice {self.name} {self.device_id} {self.max_frequency} {self.memory_size} {self.description}>'
    
class DataSubset(db.Model):
    __tablename__='data_subsets'
    id=db.Column(db.Integer,primary_key=True)
    subdir=db.Column(db.String(10),default='.')
    trn_ext=db.Column(db.String(10))
    trn_dir=db.Column(db.String(10),default='.')
    trn_file=db.Column(db.String(20),default='')
    line_no=db.Column(db.Integer)
    trn_file_format=db.Column(db.Integer) # 1 for txt,2 for json, see manage_dataset.py/gen_summary_from_dict
    num=db.Column(db.Integer)
    audio_duration_mean=db.Column(db.Float)
    audio_duration_median=db.Column(db.Float)
    audio_duration_mode=db.Column(db.Float)
    audio_duration_sum=db.Column(db.Float)
    audio_duration_max=db.Column(db.Float)
    sample_rate=db.Column(db.Integer)
    channel_num=db.Column(db.Integer)
    sample_width=db.Column(db.Integer)

class Dataset(db.Model):
    __tablename__='datasets'
    id=db.Column(db.Integer,primary_key=True)
    name=db.Column(db.String(10),index=True)
    description=db.Column(db.Text())
    path=db.Column(db.Text())
    wav_ext=db.Column(db.String(20),default='.wav')
    wav2trn_type_is_summary=db.Column(db.Boolean) # see manage_dataset.py:44
    train=db.Column(db.Integer,db.ForeignKey('data_subsets.id',onupdate="CASCADE", ondelete="CASCADE"))
    val=db.Column(db.Integer,db.ForeignKey('data_subsets.id',onupdate="CASCADE", ondelete="CASCADE"))
    test=db.Column(db.Integer,db.ForeignKey('data_subsets.id',onupdate="CASCADE", ondelete="CASCADE"))
    last_active=db.Column(db.DateTime(),default=datetime.now)

    def active(self):
        self.last_active=datetime.now()
        db.session.add(self)
        try:
            db.session.commit()
        except:
            print('无法更新数据集信息到后台')
            db.session.rollback()

class FPFE(db.Model):
    __tablename__='fpfes'
    id=db.Column(db.Integer,primary_key=True)
    name=db.Column(db.String(20),index=True)
    typ=db.Column(db.Integer) # 0 for fp,1 for fe,2 for fpfe
    modulename=db.Column(db.String(50))
    classname=db.Column(db.String(50))
    parameters=db.Column(db.Text())
    attribute=db.Column(db.Text())
    last_active=db.Column(db.DateTime(),default=datetime.now)

    def active(self):
        self.last_active=datetime.now()
        db.session.add(self)
        try:
            db.session.commit()
        except:
            print('无法更新前端处理器+特征提取器信息到后台')
            db.session.rollback()

class DataPreprocessor(db.Model):
    __tablename__='data_preprocessors'
    id=db.Column(db.Integer,primary_key=True)
    name=db.Column(db.String(20),index=True)
    modulename=db.Column(db.String(50))
    classname=db.Column(db.String(50))
    parameters=db.Column(db.Text())
    attribute=db.Column(db.Text())
    last_active=db.Column(db.DateTime(),default=datetime.now)

    def active(self):
        self.last_active=datetime.now()
        db.session.add(self)
        try:
            db.session.commit()
        except:
            print('无法更新数据预处理器信息到后台')
            db.session.rollback()

class AcousticModel(db.Model):
    __tablename__='acoustic_models'
    id=db.Column(db.Integer,primary_key=True)
    name=db.Column(db.String(20),index=True)
    typ=db.Column(db.Integer) # 0 for DL,1 for dnn+hmm,2 for gmm+hmm
    filetype=db.Column(db.Integer) # 0 for source, 1 for framework_save
    content=db.Column(db.Integer) # 0 for structure, 1 for structure+weights
    weights_file=db.Column(db.Text())
    weights_type=db.Column(db.Integer) # 0 for keras_weights, 1 for keras_ckpt, 2 for torch_state_dict
    file=db.Column(db.Text())
    modulename=db.Column(db.String(50))
    classname=db.Column(db.String(50))
    infer_classname=db.Column(db.String(50))
    infer_parameters=db.Column(db.Text())
    infer_attribute=db.Column(db.Text())
    infer_input_layer_names=db.Column(db.Text())
    infer_output_layer_names=db.Column(db.Text())
    optimizer_modulename=db.Column(db.String(50))
    optimizer_classname=db.Column(db.String(50))
    optimizer_parameters=db.Column(db.Text())
    optimizer_attribute=db.Column(db.Text())
    loss_modulename=db.Column(db.String(50))
    loss_classname=db.Column(db.String(50))
    loss_parameters=db.Column(db.Text())
    loss_attribute=db.Column(db.Text())
    parameters=db.Column(db.Text())
    attribute=db.Column(db.Text())
    file_ext=db.Column(db.Integer) # 见utils/common.py:102 0 for saved_model,1 for json, 2 for config，读模型结构 3 for torch_model
    framework=db.Column(db.Integer) # 0 for tf.keras,1 for torch.nn,2 for transformers.Wav2Vec2ForCTC
    structure=db.Column(db.Text())
    inputs=db.Column(db.Text())
    outputs=db.Column(db.Text())
    param_total=db.Column(db.Integer)
    param_trainable=db.Column(db.Integer)
    param_non=db.Column(db.Integer)
    flops=db.Column(db.Float)
    note=db.Column(db.Text(),default='') # 备注
    last_active=db.Column(db.DateTime(),default=datetime.now)

    def active(self):
        self.last_active=datetime.now()
        db.session.add(self)
        try:
            db.session.commit()
        except Exception as e:
            print(f'无法更新声学模型信息到后台, cause:{repr(e)}')
            db.session.rollback()

class LexiconDict(db.Model):
    __tablename__='lexicon_dicts'
    id=db.Column(db.Integer,primary_key=True)
    name=db.Column(db.String(20),index=True)
    g2p_dict_file=db.Column(db.Text())
    g2p_modulename=db.Column(db.String(50))
    g2p_classname=db.Column(db.String(50))
    g2p_parameters=db.Column(db.Text())
    g2p_attribute=db.Column(db.Text())
    g2p_dict_modulename=db.Column(db.String(50))
    g2p_dict_classname=db.Column(db.String(50))
    g2p_dict_parameters=db.Column(db.Text())
    g2p_dict_attribute=db.Column(db.Text())
    penc_dict_file=db.Column(db.Text())
    penc_modulename=db.Column(db.String(50))
    penc_classname=db.Column(db.String(50))
    penc_parameters=db.Column(db.Text())
    penc_attribute=db.Column(db.Text())
    penc_dict_modulename=db.Column(db.String(50))
    penc_dict_classname=db.Column(db.String(50))
    penc_dict_parameters=db.Column(db.Text())
    penc_dict_attribute=db.Column(db.Text())
    pdec_dict_file=db.Column(db.Text())
    pdec_modulename=db.Column(db.String(50))
    pdec_classname=db.Column(db.String(50))
    pdec_parameters=db.Column(db.Text())
    pdec_attribute=db.Column(db.Text())
    pdec_dict_modulename=db.Column(db.String(50))
    pdec_dict_classname=db.Column(db.String(50))
    pdec_dict_parameters=db.Column(db.Text())
    pdec_dict_attribute=db.Column(db.Text())
    p2g_dict_file=db.Column(db.Text())
    p2g_modulename=db.Column(db.String(50))
    p2g_classname=db.Column(db.String(50))
    p2g_parameters=db.Column(db.Text())
    p2g_attribute=db.Column(db.Text())
    p2g_dict_modulename=db.Column(db.String(50))
    p2g_dict_classname=db.Column(db.String(50))
    p2g_dict_parameters=db.Column(db.Text())
    p2g_dict_attribute=db.Column(db.Text())
    last_active=db.Column(db.DateTime(),default=datetime.now)

    def active(self):
        self.last_active=datetime.now()
        db.session.add(self)
        try:
            db.session.commit()
        except:
            print('无法更新发音字典信息到后台')
            db.session.rollback()

class LanguageModel(db.Model):
    __tablename__='language_models'
    id=db.Column(db.Integer,primary_key=True)
    name=db.Column(db.String(20),index=True)
    typ=db.Column(db.Integer) # 0 for ngram, 1 for dl
    file=db.Column(db.Text())
    filetype=db.Column(db.Integer) # 0 for source, 1 for framework_save
    content=db.Column(db.Integer) # 0 for structure,1 for structure+weights(一般来说只有1这个选项，除非我会继续实现加载语言模型模型文件并获取推理接口的功能)
    weights_file=db.Column(db.Text())
    weights_type=db.Column(db.Integer) # 0 for keras_weights, 1 for keras_ckpt, 2 for torch_state_dict
    file_ext=db.Column(db.Integer) # 见utils/common.py:115 0 for saved_model,1 for json, 2 for config，读模型结构 3 for torch_model, -1 for 源代码文件
    framework=db.Column(db.Integer) # 0 for tf.keras,1 for torch.nn, -1 for ngram
    modulename=db.Column(db.String(50))
    classname=db.Column(db.String(50))
    parameters=db.Column(db.Text())
    attribute=db.Column(db.Text())
    last_active=db.Column(db.DateTime(),default=datetime.now)

    def active(self):
        self.last_active=datetime.now()
        db.session.add(self)
        try:
            db.session.commit()
        except:
            print('无法更新语言模型信息到后台')
            db.session.rollback()

class Decoder(db.Model):
    __tablename__='decoders'
    id=db.Column(db.Integer,primary_key=True)
    name=db.Column(db.String(20),index=True)
    modulename=db.Column(db.String(50))
    classname=db.Column(db.String(50))
    parameters=db.Column(db.Text())
    attribute=db.Column(db.Text())
    last_active=db.Column(db.DateTime(),default=datetime.now)

    def active(self):
        self.last_active=datetime.now()
        db.session.add(self)
        try:
            db.session.commit()
        except:
            print('无法更新解码器信息到后台')
            db.session.rollback()

class Model(db.Model):
    __tablename__='models'
    id=db.Column(db.Integer,primary_key=True)
    name=db.Column(db.String(20),index=True)
    acoustic_model_id=db.Column(db.Integer,db.ForeignKey('acoustic_models.id',onupdate="CASCADE", ondelete="CASCADE"))
    lexicon_dict_id=db.Column(db.Integer,default=-1)
    lm_id=db.Column(db.Integer,default=-1)
    decoder_id=db.Column(db.Integer,default=-1)
    note=db.Column(db.Text(),default='') # 备注
    last_active=db.Column(db.DateTime(),default=datetime.now)

    def active(self):
        self.last_active=datetime.now()
        db.session.add(self)
        try:
            db.session.commit()
        except:
            print('无法更新语音识别模型信息到后台')
            db.session.rollback()

class PostProcessor(db.Model):
    __tablename__='post_processors'
    id=db.Column(db.Integer,primary_key=True)
    name=db.Column(db.String(20),index=True)
    modulename=db.Column(db.String(50))
    classname=db.Column(db.String(50))
    parameters=db.Column(db.Text())
    attribute=db.Column(db.Text())
    last_active=db.Column(db.DateTime(),default=datetime.now)

    def active(self):
        self.last_active=datetime.now()
        db.session.add(self)
        try:
            db.session.commit()
        except:
            print('无法更新后处理器信息到后台')
            db.session.rollback()

class BTTask(db.Model):
    __tablename__='bt_tasks'
    id=db.Column(db.Integer,primary_key=True)
    name=db.Column(db.String(20),index=True)
    typ=db.Column(db.Integer) # 0 for train,1 for infer
    device_id=db.Column(db.Integer)
    metrics=db.Column(db.Text())
    audio_conversion=db.Column(db.Text())
    model_save_dir=db.Column(db.Text())
    model_save_style=db.Column(db.Integer,default=-1) # 0 for weights,1 for weights+structure,-1 for infer which don't need model saved
    train_stop_criterion_category=db.Column(db.Integer) # 0 for delta_loss,1 for val_accuracy,2 for train_time, 3 for iterations,-1 for infer
    train_stop_criterion_threshold=db.Column(db.String(10))
    train_stop_criterion_times=db.Column(db.Integer,default=0)
    infer_stop_criterion_category=db.Column(db.Integer) # 0 for data_num_epoch,1 for infer_time, -1 for train
    infer_stop_criterion_threshold=db.Column(db.Float)
    infer_scenario_category=db.Column(db.Integer) # 0 for online, 1 for offline,-1 for train
    infer_scenario_client_num=db.Column(db.Integer)
    infer_scenario_request_interval_distribution=db.Column(db.Integer) # 0 for uniform,1 for normal,2 for const,3 for poisson,4 for real-time,-1 for train
    infer_scenario_request_interval_distribution_params=db.Column(db.Text())
    dataset_id=db.Column(db.Integer,db.ForeignKey('datasets.id',onupdate="CASCADE", ondelete="CASCADE"))
    summary_again=db.Column(db.Boolean,default=False)
    maintain_data_all=db.Column(db.Boolean,default=False)
    fp_id=db.Column(db.Integer,db.ForeignKey('fpfes.id',onupdate="CASCADE", ondelete="CASCADE"))
    fe_id=db.Column(db.Integer,default=-1)
    train_data_preprocessor_id=db.Column(db.Integer)
    val_data_preprocessor_id=db.Column(db.Integer)
    test_data_preprocessor_id=db.Column(db.Integer)
    post_processor_id=db.Column(db.Integer)
    model_id=db.Column(db.Integer,db.ForeignKey('models.id',onupdate='CASCADE',ondelete='CASCADE'))
    batch_size=db.Column(db.Integer)
    checkpoint_iters=db.Column(db.String(20))
    train_data_num=db.Column(db.Integer)
    val_data_num=db.Column(db.Integer)
    test_data_num=db.Column(db.Integer)
    status=db.Column(db.Integer)# 0 for releasing,1 for waiting,2 for updating,3 for paused,4 for finished,5 for abandoned,6 for executing,7 for error,8 for wait2queue
    save_ckpt_interval=db.Column(db.Integer)
    hardware_cost_collection_interval=db.Column(db.Integer)
    note=db.Column(db.Text(),default='') # 备注
    last_active=db.Column(db.DateTime(),default=datetime.now)

    def active(self):
        self.last_active=datetime.now()
        db.session.add(self)
        try:
            db.session.commit()
        except:
            print('无法更新基准测试任务信息到后台')
            db.session.rollback()

class BTExecute(db.Model):
    __tablename__='bt_executions'
    id=db.Column(db.Integer,primary_key=True)
    bttask_id=db.Column(db.Integer,db.ForeignKey('bt_tasks.id',onupdate="CASCADE", ondelete="CASCADE"))
    name=db.Column(db.String(45))
    metrics=db.Column(db.Text())
    result_status=db.Column(db.Integer)
    start_time=db.Column(db.DateTime(),default=datetime.now)
    celery_task_id=db.Column(db.String(36))
    exec_id=db.Column(db.Integer)
    tip=db.Column(db.Text(),default='') # 错误信息，执行日志
    note=db.Column(db.Text(),default='') # 备注
    end_time=db.Column(db.DateTime(),default=datetime.now)

class CompResult(db.Model):
    __tablename__='comp_result'
    id=db.Column(db.Integer,primary_key=True)
    name=db.Column(db.String(72))
    btexecs=db.Column(db.String(30))
    metrics=db.Column(db.String(30))
    note=db.Column(db.Text(),default='')
    last_active=db.Column(db.DateTime(),default=datetime.now)

    def active(self):
        self.last_active=datetime.now()
        db.session.add(self)
        try:
            db.session.commit()
        except:
            print('无法更新模型指标数值比对结果信息到后台')
            db.session.rollback()

class FileDire(db.Model):
    __tablename__='file_dire'
    id=db.Column(db.Integer,primary_key=True)
    value=db.Column(db.Text())

class TestProject(db.Model):
    __tablename__='test_projects'
    id=db.Column(db.Integer,primary_key=True)
    name=db.Column(db.String(20),index=True)
    model_id=db.Column(db.Integer,db.ForeignKey('models.id',onupdate="CASCADE", ondelete="CASCADE"))
    fp_id=db.Column(db.Integer,db.ForeignKey('fpfes.id',onupdate="CASCADE", ondelete="CASCADE"))
    fe_id=db.Column(db.Integer,default=-1)
    train_dp_id=db.Column(db.Integer,default=-1)
    val_dp_id=db.Column(db.Integer,default=-1)
    test_dp_id=db.Column(db.Integer,default=-1)
    last_active=db.Column(db.DateTime(),default=datetime.now)

    def active(self):
        self.last_active=datetime.now()
        db.session.add(self)
        try:
            db.session.commit()
        except:
            print('无法更新测试项目信息到后台')
            db.session.rollback()

class TPToUC(db.Model):
    __tablename__='tp_to_ucs'
    id=db.Column(db.Integer,primary_key=True)
    tp_id=db.Column(db.Integer,db.ForeignKey('test_projects.id',onupdate='CASCADE',ondelete='CASCADE'))
    uc_id=db.Column(db.Integer,db.ForeignKey('bt_tasks.id',onupdate='CASCADE',ondelete='CASCADE'))
